﻿using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;

namespace Reader.Core
{
    public class EmbeddedZipFileProvider : IFileProvider
    {
        private static readonly char[] _invalidFileNameChars = Path.GetInvalidFileNameChars()
            .Where(c => c != '/' && c != '\\').ToArray();
        private readonly Assembly _assembly;
        private readonly string _zipPath;
        private readonly DateTimeOffset _lastModified;


        public EmbeddedZipFileProvider(Assembly assembly, string zipPath/*, string baseNamespace*/)
        {
            _assembly = assembly ?? throw new ArgumentNullException("assembly");
            _zipPath = zipPath.TrimStart('/', '\\').Replace("/", ".").Replace("\\", ".") ?? throw new ArgumentNullException("zip file path");
            _lastModified = DateTimeOffset.UtcNow;

            if (!string.IsNullOrEmpty(_assembly.Location))
            {
                try
                {
                    _lastModified = File.GetLastWriteTimeUtc(_assembly.Location);
                }
                catch (PathTooLongException) { }
                catch (UnauthorizedAccessException) { }
            }
        }

        private static bool HasInvalidPathChars(string path)
        {
            return path.IndexOfAny(_invalidFileNameChars) != -1;
        }

        public IDirectoryContents GetDirectoryContents(string subpath)
        {
            if(subpath == null)
            {
                return NotFoundDirectoryContents.Singleton;
            }
            if (subpath.Length != 0 && !string.Equals(subpath, "/", StringComparison.Ordinal))
            {
                return NotFoundDirectoryContents.Singleton;
            }

            var entries = new List<IFileInfo>();

            var resources = _assembly.GetManifestResourceNames();
            var nameSpace = _assembly.EntryPoint.DeclaringType?.Namespace;
            var path = resources.SingleOrDefault(it => it == $"{nameSpace}.{_zipPath}");
            if (string.IsNullOrEmpty(path))
            {
                return NotFoundDirectoryContents.Singleton;
            }
            using (var stream = _assembly.GetManifestResourceStream(path))
            {
                if (stream == null) throw new FileNotFoundException($"embedd resource {_zipPath} not found");
                Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
                using (var archive = new ZipArchive(stream, ZipArchiveMode.Read, false, Encoding.GetEncoding("GBK")))
                {
                    foreach (var entry in archive.Entries)
                    {
                        if(entry.ExternalAttributes != 16)
                        {
                            entries.Add(new EmbeddedResourceZipFileInfo(
                            _assembly,
                            _zipPath,
                            entry.FullName,
                            entry.Name,
                            entry.Length,
                            entry.LastWriteTime
                            ));
                        }
                    }
                }
            }
            
            return new EnumerableDirectoryContents(entries);
        }

        public IFileInfo GetFileInfo(string subpath)
        {
            if (string.IsNullOrEmpty(subpath))
            {
                return new NotFoundFileInfo(subpath);
            }
            if(subpath.StartsWith("/", StringComparison.Ordinal))
            {
                subpath = subpath.Substring(1);
            }
            //subpath = subpath.Replace('/', '.').Replace('\\', '.');
            if (HasInvalidPathChars(subpath))
            {
                return new NotFoundFileInfo(subpath);
            }
            var resources = _assembly.GetManifestResourceNames();
            var nameSpace = _assembly.EntryPoint.DeclaringType?.Namespace;
            var path = resources.SingleOrDefault(it => it == $"{nameSpace}.{_zipPath}");
            if (string.IsNullOrEmpty(path))
            {
                return new NotFoundFileInfo(path);
            }
            using (var stream = _assembly.GetManifestResourceStream(path))
            {
                if (stream == null) return new NotFoundFileInfo(subpath);
                Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
                using(var archive = new ZipArchive(stream, ZipArchiveMode.Read, false, Encoding.GetEncoding("GBK")))
                {
                    var entry = archive.Entries.SingleOrDefault(e => e.FullName == subpath);
                    if(entry == null)
                    {
                        return new NotFoundFileInfo(subpath);
                    }
                    return new EmbeddedResourceZipFileInfo(
                        _assembly,
                        _zipPath,
                        entry.FullName,
                        entry.Name,
                        entry.Length,
                        entry.LastWriteTime
                        );
                }
            }
        }

        public IChangeToken Watch(string filter)
        {
            return NullChangeToken.Singleton;
        }
    }

    internal class EnumerableDirectoryContents : IDirectoryContents
    {
        private readonly IEnumerable<IFileInfo> _entries;

        public EnumerableDirectoryContents(IEnumerable<IFileInfo> entries)
        {
            _entries = entries ?? throw new ArgumentNullException(nameof(entries));
        }

        public bool Exists
        {
            get { return true; }
        }

        public IEnumerator<IFileInfo> GetEnumerator()
        {
            return _entries.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return _entries.GetEnumerator();
        }
    }
}
